import sax = require("sax");
import {QualifiedAttribute, QualifiedTag, Tag} from "sax";

interface BlocklyParseStatus {
    onStart(element_name: string, attributes: Map<string, string>): BlocklyParseStatus;

    onEnd(): BlocklyParseStatus;

    onAttribute(name: string, value: string): void;

    onText(text: string): void
}

interface IField {
    getName(): string;

    getValue(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): any;
}

function instanceOfIField(object: any): object is IField {
    return 'getName' in object && 'getValue' in object;
}

interface ILeftOutput {
    leftOutput(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): any;
}

function instanceOfILeftOutput(object: any): object is ILeftOutput {
    return 'leftOutput' in object;
}

interface IExecutable {
    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void;
}

function instanceOfIExecutable(object: any): object is IExecutable {
    return 'execute' in object;
}

interface IDefinition {
    define(runtime: BlocklyRuntime): void;
}

function instanceOfIDefinition(object: any): object is IDefinition {
    return "define" in object;
}

interface IStart {
    onEvent(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void;
}

function instanceOfIStart(object: any): object is IStart {
    return "onEvent" in object;
}

interface ISettable {
    setValue(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>, value: any): void;
}

function instanceOfISettable(object: any): object is ISettable {
    return "setValue" in object;
}

interface Identified {
    getId(): string;
}

function instanceOfIdentified(object: any): object is Identified {
    return "getId" in object;
}

interface ICallable {
    makeCall(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>, args: Map<string, any>): any | void;
}

class BlocklyRuntime {
    rootStatus: RootStatus
    variables: Map<string, { name: string, value: any }> = new Map<string, { name: string; value: any }>()
    currentId: string | undefined = undefined
    functions: Map<string, ICallable> = new Map<string, ICallable>()

    constructor(rootStatus: RootStatus) {
        this.rootStatus = rootStatus;
    }

    print(text: string) {
        console.log(text);
    }

    setVariable(id: string, name: string, value: any) {
        this.variables.set(id, {name: name, value: value});
    }

    getVariable(id: string): any | undefined {
        let variable = this.variables.get(id);
        if (variable != undefined) {
            return variable.value;
        } else {
            return undefined;
        }
    }

    getLeftValue(leftout: ILeftOutput, event: string, parameters: Map<string, any>): any {
        let id = this.currentId;
        if (leftout instanceof BaseNode) {
            this.currentId = leftout.getBlockId();
        }
        let value = leftout.leftOutput(this, event, parameters);
        this.currentId = id;
        return value;
    }

    executeBlock(executable: IExecutable, event: string, parameters: Map<string, any>): void {
        let id = this.currentId;
        if (executable instanceof BaseNode) {
            this.currentId = executable.getBlockId();
        }
        executable.execute(this, event, parameters);
        if (executable instanceof BaseNode) {
            if (executable.next != null) {
                this.executeBlock(executable.next, event, parameters);
            }
        }
        this.currentId = id;
    }

    defineFunction(name: string, func: ICallable): void {
        this.functions.set(name, func);
    }

    callReturn(name: string, event: string, parameters: Map<string, any>, args: Map<string, any>): any | void | undefined {
        let func = this.functions.get(name);
        if (func != undefined) {
            func.makeCall(this, event, parameters, args);
        }
    }

    execute(event: string, parameters: Map<string, any>) {
        this.rootStatus.start(this, event, parameters);
    }
}

abstract class BaseNode implements BlocklyParseStatus {
    mutation: Mutation | null;
    parser: BlocklyParser;
    parent: BlocklyParseStatus;
    fields: Map<string, IField>;
    attributes: Map<string, string>;
    leftout: ILeftOutput | null = null;
    children: BlocklyParseStatus[];
    data: string = "";
    statementMap: Map<string, Statement>;
    next: NextNode | null;


    constructor(parser: BlocklyParser, parent: BlocklyParseStatus) {
        this.parser = parser;
        this.parent = parent;
        this.fields = new Map<string, IField>();
        this.attributes = new Map<string, string>();
        this.children = [];
        this.statementMap = new Map<string, Statement>();
        this.mutation = null;
        this.next = null;
    }

    onAttribute(name: string, value: string): void {
        this.attributes.set(name.toLowerCase(), value);
    }

    onEnd(): BlocklyParseStatus {
        return this.parent;
    }

    onStart(element_name: string, attributes: Map<string, string>): BlocklyParseStatus {
        let node = BlocklyNodeFactory.createNode(this.parser, this, element_name, attributes);
        attributes.forEach((value, key) => {
            node.onAttribute(key, value);
        });
        if (instanceOfIField(node)) {
            let field = node as IField;
            this.fields.set(field.getName(), field);
        }
        if (node instanceof NextNode) {
            this.next = node;
        }
        if (instanceOfILeftOutput(node)) {
            this.leftout = node as ILeftOutput;
        }
        if (node instanceof Statement) {
            let statement = node as Statement;
            let name = node.getName();
            if (name != undefined) {
                this.statementMap.set(name, statement);
            }
        }
        if (node instanceof Mutation) {
            this.mutation = node as Mutation;
        }
        this.children.push(node);
        return node;
    }

    onText(text: string): void {
        this.data = text;
    }

    getFieldValue(name: string, runtime: BlocklyRuntime, event: string, parameters: Map<string, any>, defaultVal: any): any {
        let field = this.fields.get(name);
        if (field != undefined) {
            return field.getValue(runtime, event, parameters);
        } else {
            return defaultVal;
        }
    }

    setFieldValue(name: string, runtime: BlocklyRuntime, event: string, parameters: Map<string, any>, value: any): void {
        let field = this.fields.get(name);
        if (field != undefined) {
            if (instanceOfISettable(field)) {
                field.setValue(runtime, event, parameters, value);
            }
        }
    }

    getFieldId(name: string): string | undefined {
        let field = this.fields.get(name);
        if (field != undefined) {
            if (instanceOfIdentified(field)) {
                return field.getId();
            }
        }
        return undefined;
    }

    getStatement(name: string): Statement | undefined {
        return this.statementMap.get(name);
    }

    getBlockId(): string | undefined {
        return this.attributes.get("id");
    }
}

class NextNode extends BaseNode implements IExecutable {
    block: IExecutable | undefined = undefined

    onStart(element_name: string, attributes: Map<string, string>): BlocklyParseStatus {
        let node = super.onStart(element_name, attributes);
        if (instanceOfIExecutable(node)) {
            this.block = node;
        }
        return node;
    }

    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        if (this.block != undefined) {
            runtime.executeBlock(this.block, event, parameters)
        }
    }
}


class Statement extends BaseNode implements IExecutable {
    block: IExecutable | null = null;

    onStart(element_name: string, attributes: Map<string, string>): BlocklyParseStatus {
        let node = super.onStart(element_name, attributes);
        if (instanceOfIExecutable(node)) {
            this.block = node as IExecutable;
        }
        return node;
    }

    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        if (this.block != null) {
            runtime.executeBlock(this.block, event, parameters);
        }
    }

    getName(): string | undefined {
        return this.attributes.get("name");
    }
}

class ArgNode extends BaseNode {
    name(): string | undefined {
        return this.attributes.get("name");
    }

    varid(): string | undefined {
        return this.attributes.get("varid");
    }
}

class Mutation extends BaseNode {
    args: ArgNode[] = []

    items(): string | undefined {
        return this.attributes.get('items');
    }

    elseif(): number {
        let elseif = this.attributes.get("elseif")
        if(elseif == undefined) {
            return 0;
        } else {
            return parseInt(elseif);
        }
    }

    else(): string | undefined {
        return this.attributes.get("else");
    }

    name(): string | undefined {
        return this.attributes.get("name");
    }

    onStart(element_name: string, attributes: Map<string, string>): BlocklyParseStatus {
        let node = super.onStart(element_name, attributes);
        if (node instanceof ArgNode) {
            this.args.push(node);
        }
        return node;
    }

    statements(): boolean {
        let statements = this.attributes.get('statements');
        return statements == undefined || statements != "false";
    }
}

class XmlNode extends BaseNode {
    start(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>) {
        for (let index in this.children) {
            let block = this.children[index];
            if (instanceOfIDefinition(block)) {
                block.define(runtime);
            }
        }
        for (let index in this.children) {
            let block = this.children[index];
            if (instanceOfIStart(block)) {
                block.onEvent(runtime, event, parameters);
            }
        }
    }
}

class RootStatus implements BlocklyParseStatus {
    parser: BlocklyParser;
    xml: XmlNode | null = null;

    constructor(parser: BlocklyParser) {
        this.parser = parser;
    }

    onAttribute(name: string, value: string): void {
    }

    onEnd(): BlocklyParseStatus {
        return this;
    }

    onStart(element_name: string, attributes: Map<string, string>): BlocklyParseStatus {
        if (element_name == "xml") {
            this.xml = new XmlNode(this.parser, this);
            return this.xml;
        }
        return this;
    }

    onText(text: string): void {
    }

    start(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>) {
        if (this.xml != null) {
            this.xml.start(runtime, event, parameters);
        }
    }
}

interface SAXListener {
    onOpenTag(name: string, attributes: Map<string, string>): void

    onEndTag(name: string): void

    onText(text: string): void
}

class SAXParser {
    attributes: Map<string, string>
    listener: SAXListener

    constructor(listener: SAXListener) {
        this.listener = listener;
        this.attributes = new Map<string, string>();
    }

    onOpenTag(node: Tag | QualifiedTag): void {
        let name = node.name;
        this.listener.onOpenTag(name, this.attributes);
        this.attributes = new Map<string, string>();
    }

    onEndTag(tagname:string): void {
        this.listener.onEndTag(tagname);
    }

    onText(t: string): void {
        this.listener.onText(t);
    }

    onAttribute(name: string, value: string): void {
        this.attributes.set(name, value);
    }

    parse(xml: string): void {
        let xml_parser = sax.parser(true);
        xml_parser.onerror = function (e) {

        }
        xml_parser.ontext = (t) => {
            this.onText(t);
        }
        xml_parser.onopentag = (node) => {
            this.onOpenTag(node);
        }
        xml_parser.onclosetag = (tagname) => {
            this.onEndTag(tagname);
        }
        xml_parser.onattribute = (attr) => {
            this.onAttribute(attr.name, attr.value);
        }
        xml_parser.write(xml).close();
    }
}

class BlocklyParser implements SAXListener {
    status: BlocklyParseStatus
    root: RootStatus

    constructor() {
        this.root = new RootStatus(this);
        this.status = this.root;
    }

    onOpenTag(name: string, attributes: Map<string, string>): void {
        this.status = this.status.onStart(name, attributes);
    }

    onEndTag(name: string): void {
        this.status = this.status.onEnd();
    }

    onText(text: string): void {
        this.status.onText(text);
    }

    runtime(): BlocklyRuntime {
        return new BlocklyRuntime(this.root);
    }

    static parse(xml: string): BlocklyRuntime {
        let parser = new BlocklyParser();
        let sax_parser = new SAXParser(parser);
        sax_parser.parse(xml);
        return parser.runtime();
    }
}

class UnknownNode extends BaseNode implements IExecutable {
    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        runtime.print("Unknown");
    }
}

class OnStart extends BaseNode implements IStart {
    onEvent(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        if (event == 'start') {
            if (this.next != undefined) {
                runtime.executeBlock(this.next, event, parameters);
            }
        }
    }
}

class TextPrint extends BaseNode implements IExecutable {
    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        let text = this.getFieldValue("TEXT", runtime, event, parameters, "");
        runtime.print(text);
    }
}

class ValueNode extends BaseNode implements IField, ISettable {
    getName(): string {
        // @ts-ignore
        return this.attributes.get("name");
    }

    getValue(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): any {
        if (this.leftout == undefined) {
            return undefined;
        } else {
            return runtime.getLeftValue(this.leftout, event, parameters);
        }
    }

    setValue(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>, value: any): void {
        if (this.leftout == undefined) {
            return;
        } else {
            if (instanceOfISettable(this.leftout)) {
                this.leftout.setValue(runtime, event, parameters, value);
            }
        }
    }
}

class ShadowNode extends BaseNode implements ILeftOutput {
    leftOutput(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): any {
        switch (this.attributes.get("type")) {
            case "text":
                return this.getFieldValue("TEXT", runtime, event, parameters, "");
            case "math_number":
                return this.getFieldValue("NUM", runtime, event, parameters, 0);
        }
    }
}

class FieldNode extends BaseNode implements IField, Identified {
    getId(): string {
        // @ts-ignore
        return this.attributes.get("id");
    }

    getName(): string {
        // @ts-ignore
        return this.attributes.get("name");
    }

    getValue(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): any {
        return this.data;
    }

}

class TextNode extends BaseNode implements ILeftOutput {
    leftOutput(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): any {
        return this.getFieldValue("TEXT", runtime, event, parameters, "");
    }
}

class Variables extends BaseNode implements IExecutable {
    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        this.children.forEach((child) => {
            if(instanceOfIExecutable(child)) {
                runtime.executeBlock(child, event, parameters);
            }
        });
    }
}

class Variable extends BaseNode implements IExecutable {
    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        // @ts-ignore
        runtime.setVariable(this.attributes.get('id'), this.data, "");
    }
}

class ControlsIf extends BaseNode implements IExecutable {
    constructor(parser: BlocklyParser, parent: BlocklyParseStatus) {
        super(parser, parent);
        this.mutation = new Mutation(parser, parent);
    }

    execute(runtime: BlocklyRuntime, event: string, parameters: Map<string, any>): void {
        let if0 = this.getFieldValue("IF0", runtime, event, parameters,false);
        if(if0) {
            let do0 = this.getStatement("DO0");
            if(do0 != undefined) {
                runtime.executeBlock(do0, event, parameters);
            }
            return;
        }
        let elseif = this.mutation?.elseif();
        // @ts-ignore
        for(let i = 0; i < elseif; i++) {
            let ifN = this.getFieldValue(`IF${i}`, runtime, event, parameters, false);
            if(ifN) {
                let doN = this.getStatement(`DO${i}`);
                if(doN != undefined) {
                    runtime.executeBlock(doN, event, parameters);
                }
            }
        }
    }
}

//todo
class BlocklyNodeFactory {
    static createNode(parser: BlocklyParser, parent: BlocklyParseStatus, element_name: string, attributes: Map<string, string>): BlocklyParseStatus {
        switch (element_name.toLowerCase()) {
            case "block":
                let type = attributes.get('type');
                if (type != undefined) {
                    return BlocklyNodeFactory.createTypedBlock(parser, parent, element_name, type as string, attributes);
                }
                break;
            case "field":
                return new FieldNode(parser, parent);
            case "value":
                return new ValueNode(parser, parent);
            case "next":
                return new NextNode(parser, parent);
            case "shadow":
                return new ShadowNode(parser, parent);
            case "variables":
                return new Variables(parser, parent);
            case "variable":
                return new Variable(parser, parent);
            case "statement":
                return new Statement(parser, parent);
            case "mutation":
                return new Mutation(parser, parent);
            case "arg":
                return new ArgNode(parser, parent);
        }
        return new UnknownNode(parser, parent);
    }

    private static createTypedBlock(parser: BlocklyParser, parent: BlocklyParseStatus, element_name: string, type: string, attributes: Map<string, string>): BlocklyParseStatus {
        switch (type) {
            case "controls_if":
                return new ControlsIf(parser, parent);
            // case "logic_boolean":
            //     return new LogicalBoolean(parser, parent);
            // case "logic_compare":
            //     return new LogicCompare(parser, parent);
            // case "logic_negate":
            //     return new LogicNegate(parser, parent);
            // case "logic_operation":
            //     return new LogicOperation(parser, parent);
            // case "logic_null":
            //     return new LogicNull(parser, parent);
            //
            // case "controls_repeat_ext":
            //     return new ControlsRepeatExt(parser, parent);
            // case "controls_whileUntil":
            //     return new ControlsWhileUntil(parser, parent);
            // case "controls_for":
            //     return new ControlsFor(parser, parent);
            // case "controls_forEach":
            //     return new ControlsForEach(parser, parent);
            // case "controls_flow_statements":
            //     return new ControlsFlowStatements(parser, parent);
            //
            // case "math_number":
            //     return new MathNumber(parser, parent);
            // case "math_change":
            //     return new MathChange(parser, parent);
            // case "math_arithmetic":
            //     return new MathArithmetic(parser, parent);
            // case "math_single":
            //     return new MathSingle(parser, parent);
            // case "math_trig":
            //     return new MathTrig(parser, parent);
            // case "math_constant":
            //     return new MathConstant(parser, parent);
            // case "math_number_property":
            //     return new MathNumberProperty(parser, parent);
            // case "math_round":
            //     return new MathRound(parser, parent);
            // case "math_on_list":
            //     return new MathOnList(parser, parent);
            // case "math_modulo":
            //     return new MathModulo(parser, parent);
            // case "math_constrain":
            //     return new MathConstrain(parser, parent);
            // case "math_random_int":
            //     return new MathRandomInt(parser, parent);
            // case "math_random_float":
            //     return new MathRandomFloat(parser, parent);
            // case "math_atan2":
            //     return new MathAtan2(parser, parent);
            //
            case "text":
                return new TextNode(parser, parent);
            case "text_print":
                return new TextPrint(parser, parent);
            // case "text_append":
            //     return new TextAppend(parser, parent);
            // case "text_join":
            //     return new TextJoin(parser, parent);
            // case "text_length":
            //     return new TextLength(parser, parent);
            // case "text_isEmpty":
            //     return new TextIsEmpty(parser, parent);
            // case "text_indexOf":
            //     return new TextIndexOf(parser, parent);
            // case "text_charAt":
            //     return new TextCharAt(parser, parent);
            // case "text_getSubstring":
            //     return new TextGetSubstring(parser, parent);
            // case "text_changeCase":
            //     return new TextChangeCase(parser, parent);
            // case "text_trim":
            //     return new TextTrim(parser, parent);
            //
            // case "lists_create_with":
            //     return new ListsCreateWith(parser, parent);
            // case "lists_repeat":
            //     return new ListsRepeat(parser, parent);
            // case "lists_length":
            //     return new ListsLength(parser, parent);
            // case "lists_isEmpty":
            //     return new ListsIsEmpty(parser, parent);
            // case "lists_indexOf":
            //     return new ListsIndexOf(parser, parent);
            // case "lists_getIndex":
            //     return new ListsGetIndex(parser, parent);
            // case "lists_setIndex":
            //     return new ListsSetIndex(parser, parent);
            // case "lists_getSublist":
            //     return new ListsGetSublist(parser, parent);
            // case "lists_split":
            //     return new ListsSplit(parser, parent);
            // case "lists_sort":
            //     return new ListsSort(parser, parent);
            //
            // case "variables_set":
            //     return new VariablesSet(parser, parent);
            // case "variables_get":
            //     return new VariablesGet(parser, parent);
            //
            // case "procedures_defreturn":
            //     return new ProceduresDefreturn(parser, parent);
            // case "procedures_callreturn":
            //     return new ProceduresCallreturn(parser, parent);
            // case "procedures_ifreturn":
            //     return new ProceduresIfreturn(parser, parent);
            // case "procedures_defnoreturn":
            //     return new ProceduresDefnoreturn(parser, parent);

            case "onstart":
                return new OnStart(parser, parent);
        }
        return new UnknownNode(parser, parent);
    }
}

export {BlocklyParser, BlocklyNodeFactory, BlocklyRuntime}


